import {Engine} from 'node-uci'
import {Chess} from 'chess.js'
import fs from 'fs'
import _ from "lodash";
import * as utils from "./utils";
import {hex2str, invoke, replaceAndTrim, sleep, str2hex} from "./utils";
import path from 'path'
import moment from 'moment'
import Database from "@/lib/database";
import emitvue from "@/lib/emitvue";

// 引擎实例
let engine = null
let nodes = 1500000
let mistakeThreshold = 0.5
let blunderThreshold = 1

interface IAnalyzeData {
  bestmove: string,
  info: {
    score: {
      unit: string,
      value: number,
    },
    depth: number,
    seldepth: number,
    time: number,
    nodes: number,
    hashfull: number,
    nps: number,
    tbhits: number,
    pv: string,
    multipv: string,
    [key: string]: any
  }[]
}

interface IAnalyzeResult {
  bestmove: string,
  moveList: {
    move: string,
    value: number
  }[]
}

export default new class Chessmate {
  static analyzeFlag = false
  static analyzeCount = 0
  static analyzeDelete = 0

  exitAnalyze () {
    Chessmate.analyzeFlag = true
    return Chessmate.analyzeCount
  }

  getAnalyzeDelete() {
    return Chessmate.analyzeDelete
  }

  async init(config: {path: string, blunderThreshold: number, mistakeThreshold: number, analyzeSpeed: string}) {
    if (!engine) {
      engine = new Engine(config.path)
    }
    if (config.analyzeSpeed === '快速') nodes = 500000
    if (config.analyzeSpeed === '极快') nodes = 100000
    else if (config.analyzeSpeed === '普通') nodes = 1500000
    else if (config.analyzeSpeed === '精确') nodes = 3000000
    else if (config.analyzeSpeed === '极慢') nodes = 5000000
    if (config.mistakeThreshold) mistakeThreshold = Number(config.mistakeThreshold)
    if (config.blunderThreshold) blunderThreshold = Number(config.blunderThreshold)
  }

  async analyze(fen: string, userNodes=null): Promise<IAnalyzeData> {
    await engine.init()
    await engine.setoption('MultiPV', '4')
    await engine.isready()
    await engine.position(fen)
    const result = await engine.go( {nodes: userNodes || nodes})
    // fs.writeFileSync('./result.json', JSON.stringify(result, null, 4))
    await engine.quit()
    return result
  }

  // 与analyze不同的是分析的是当前局面下一步最佳落子的评分，一般是引擎没有选择这步棋，强行分析
  async analyzeCurrentFenValue(fen: string, move=null, nodes=null): Promise<{move: string, value: any, [key: string]: any}> {
    if (move) {
      fen = this.move(fen, move)
    }
    const result = await this.analyze(fen, nodes)
    return this.getAnalyzeBestMove(fen, result)
  }

  // 对于引擎分析的结果 只获取最佳落子和分值
  getAnalyzeBestMove(fen: string, data: IAnalyzeData): {move: string, value: string, [key: string]: any} {
    const bestmove = data.bestmove
    data.info = _.reverse(data.info)
    let isWhite = this.isWhite(fen)
    let acceptedMove: any = {}
    // 先找最佳点位
    for (const item of data.info) {
      // 局面被将杀或者逼和才会进行分析
      if (!item.pv) {
        return {move:  null, uciMove: null, pv: [], value: "0", valueText: "0"}
      }
      const pv = item.pv.split(' ')
      if (pv[0] === bestmove) {
        acceptedMove[pv[0]] = true
        let value
        if (item.score.unit === 'cp') {
          value = item.score.value/100
          if (!isWhite) value = -value
        }
        else if (item.score.unit === 'mate') {
          value = Math.abs(item.score.value)
          value = 'M' + value
          if (!isWhite) value = "-" + value
          else if (item.score.value < 0) value = "-" + value
        }
        return {move:  this.parseUCIMove(fen, pv[0]), uciMove: pv[0], pv, value, valueText: value}
      }
    }
    return null
  }


  parseAnalyze(fen: string, data: IAnalyzeData): IAnalyzeResult {
    const bestmove = data.bestmove
    data.info = _.reverse(data.info)
    let isWhite = this.isWhite(fen)
    let maxDepth = data.info[0].depth
    let moveList: any = []
    let acceptedMove: any = {}
    let bestValue = null
    // 先找最佳点位
    for (const item of data.info) {
      const pv = item.pv.split(' ')
      if (pv[0] === bestmove) {
        acceptedMove[pv[0]] = true
        let value: any = 0
        if (item.score.unit === 'cp') {
          value = item.score.value/100
          if (!isWhite) value = -value
        }
        else if (item.score.unit === 'mate') {
          value = Math.abs(item.score.value)
          value = 'M' + value
          if (!isWhite) value = "-" + value
          else if (item.score.value < 0) value = "-" + value
        }
        bestValue = value
        moveList.push({move:  this.parseUCIMove(fen, pv[0]), uciMove: pv[0], pv, value, valueText: value})
        break
      }
    }
    for (const item of data.info) {
      if (item.depth < (maxDepth - 2)) break   // 深度过低
      if(!item.pv) continue
      const pv = item.pv.split(' ')
      if (acceptedMove[pv[0]]) continue    // 说明已经采纳这个走法了
      acceptedMove[pv[0]] = true
      let value
      if (item.score.unit === 'cp') {
        value = item.score.value/100
        if (!isWhite) value = -value
      }
      else if (item.score.unit === 'mate') {
        value = Math.abs(item.score.value)
        value = 'M' + value
        if (!isWhite) value = "-" + value
        else if (item.score.value < 0) value = "-" + value
      }
      let valueText: string = null
      if (item.score.unit === 'cp') {
        if (Math.abs(bestValue - value) > mistakeThreshold) valueText = `${value} (mistake)`
        if (Math.abs(bestValue - value) > blunderThreshold) valueText = `${value} (blunder)`
      }

      if (!valueText) valueText = String(value)
      moveList.push({move: this.parseUCIMove(fen, pv[0]), uciMove: pv[0], pv, value, valueText})
    }
    return {bestmove: this.parseUCIMove(fen, bestmove), moveList}
  }


  // 判断目前是黑棋还是白棋在走
  isWhite(fen: string): boolean {
    const step = fen.split(' ')
    return !step || step[1] === 'w';
  }

  // 获取一个局面有效的步骤数
  getInvalidMoveList(fen: string): string[] {
    const chess = new Chess(fen)
    return chess.moves()
  }

  // 将形如e2e4的棋步解析成e4
  parseUCIMove(fen: string, move: string): string {
    const chess = new Chess(fen)
    chess.move(move)
    const history = chess.history({ verbose: true })
    return history[0].san
  }

  // 将形如e4的棋步解析成e2e4
  getUCIMove(fen: string, move: string): string {
    const chess = new Chess(fen)
    chess.move(move)
    const history = chess.history({ verbose: true })
    return history[0].lan
  }

  // 返回一个当前局面+棋步的fen
  move(fen: string, move: string): string {
    const chess = new Chess(fen)
    if (move) {
      chess.move(move)
    }
    return chess.fen()
  }

  // 将当前的得分翻转黑白
  reverseValue(fen: string, value: string, bestValue=null): string {
    const isWhite = this.isWhite(fen)
    let newValue: any = value
    if (String(value).indexOf("M") === -1) {
      if (isWhite) newValue = -value
    }
    else {
      newValue = Math.abs(Number(value))
      newValue = 'M' + value
      if (!isWhite) newValue = "-" + newValue
      else if (Number(newValue) < 0) newValue = "-" + newValue
    }
    console.log("newValue", newValue)
    let valueText: string = null
    if (bestValue !== null) {
      if (String(value).indexOf("M") === -1) {
        if (Math.abs(bestValue - newValue) > mistakeThreshold) valueText = `${newValue} (mistake)`
        if (Math.abs(bestValue - newValue) > blunderThreshold) valueText = `${newValue} (blunder)`
      }
    }
    return valueText
  }

  // 判断这个评分和最佳评分的差别
  judgeValue(value: any, bestValue: any): string {
    let valueText = String(value)
    if (String(value).indexOf("M") === -1) {
      if (Math.abs(value - bestValue) > mistakeThreshold) valueText = `${value} (mistake)`
      if (Math.abs(value - bestValue) > blunderThreshold) valueText = `${value} (blunder)`
    }
    return valueText
  }


  // 根据传入的开局信息，生成实战的数据
  generateReadModeData(openingList: {data: { move: string, description: string }[], fen: string, name: string, subName: string, descriptionCount: number}[]): {
    endGameMap: {[key: string]: string}, fenMap: {[key: string]: string[]}
  } {
    const res: {[key: string]: string[]} = {} as any
    const endGameMap: {[key: string]: string} = {} as any
    for (const opening of openingList) {
      const {data} = opening
      // 棋谱不是按上下的走法去决定的，而是任何一个相同的fen都视作同一个局面，而我们要计算的是这些局面有哪些走法。
      let currentFen = utils.newGame
      for (let i=0; i<data.length; i++) {
        if (!res[currentFen]) res[currentFen] = []
        res[currentFen].push(data[i].move)
        currentFen = this.move(currentFen, data[i].move)
        if (i=== data.length - 1) {
          endGameMap[currentFen] = opening.subName
        }
      }
    }
    for (const key in res) {
      res[key] = _.uniq(res[key])
      if (endGameMap[key]) delete endGameMap[key]
    }
    // 如果在终局的数据里面，在对局信息里依然有值，说明这个开局可能只是一小段，而后面还有后续，则走到这里不能算是终局
    return {fenMap: res, endGameMap}
  }

  getAscii(fen: string, move?: string): string {
    const chess = new Chess(fen)
    if (move) {
      chess.move(move)
    }
    return chess.ascii()
  }

  // 导出成pdf
  async exportPgn2Pdf(moveList: { move: string }[], name: string, subName: string, filePath: string, pluginPath: string): Promise<string> {
    let pgn = ''
    let ascii = ''
    let initialFen = utils.newGame
    let count = 1
    for (let i=0; i<moveList.length; i++) {
      if (i % 2 === 0) {
        pgn += count + '.'
      }
      const move = moveList[i].move
      pgn += move + ' '
      // 请注意执行的先后顺序
      const tempAsc = this.getAscii(initialFen, move)
      initialFen = this.move(initialFen, move)
      if (i%2 === 0) {
        ascii += `${count}.`
      } else {
        ascii += `${count}...`
      }
      ascii += move
      ascii += '\n'
      ascii += '\n'
      ascii += tempAsc
      ascii += '\n'
      ascii += '\n'
      if (i % 2 !==0) count ++
      // 勿删，是转pdf插件识别分页的数据
      if (i % 3 === 0) {
        ascii += '*****\n'
      }
    }
    pgn += '\n\n\n\n' + ascii
    let filename = `${name} ${subName}.txt`
    filePath = path.join(filePath, filename)
    filePath = filePath.replaceAll(' ', '_')
    fs.writeFileSync(filePath, pgn)
    const nodePath = path.join(pluginPath, './node.exe')
    const txtParsePath = path.join(pluginPath, './txt2pdf/txt2pdf.js')
    let cmd = `${nodePath} ${txtParsePath} ${filePath}`
    console.log("cmd", cmd)
    invoke('exec', {cmd})
    return pgn
  }
  
  // 解析pgn数据并入库
  async parsePgnStore(pgn: string): Promise<{ gameCount: number, jumpCount: number }> {
    let gameCount = 0
    let jumpCount = 0
    const lines = pgn.split('\n')
    // 匹配是否包含中括号
    let pattern1: any = /\[{1}(.+)]/
    // Lichess的规则，先从Pgn开始，第一个空行后跟着的是棋步，再过两个空行后跟着的是下一盘棋
    let gameInfo = {
      event: '',
      type: '',
      date: '',
      opening: '',
      white: '',
      black: '',
      result: '',
      whiteElo: '',
      blackElo: '',
      time: '',
      hash: '',
      data: [],
    }
    // 为了初始化
    const initGameInfo = _.cloneDeep(gameInfo)
    let jumpFlag = false
    for (const line of lines) {
      if (!line || line === '') continue
      // 如果直接存了一个结果而没有棋步，说明是特殊对局，如开始就弃权，超时等等
      if (line.trim() === '0-1' || line.trim() === '1-0' || line.trim() === '1/2-1/2') {
        gameInfo = _.cloneDeep(initGameInfo)
        jumpCount++
        continue
      }
      // 遇到对局信息，说明来到了一盘棋的末尾，下一条数据就是新的一盘棋了
      // 且不能是时间或者角色名等数据，防止截取错误
      if ((line.indexOf("1.") !== -1) && line.indexOf('[') === -1)  {
        gameInfo.data = this.parseFgnMove(line)
        const count = await Database.findGameByHash(gameInfo.hash)
        // 重复的不会入库
        if (count && count[0].count !== 0) {
          gameInfo = _.cloneDeep(initGameInfo)
          continue
        }
        // 跳过特殊对局还需要重新初始化
        if (jumpFlag) {
          gameInfo = _.cloneDeep(initGameInfo)
          jumpFlag = false
          continue
        }
        await Database.saveUserGame(gameInfo)
        // 入库之后开始计算下一个对局
        gameInfo = _.cloneDeep(initGameInfo)
        gameCount++
      }
      // 先匹配出方括号内的内容
      const data = pattern1.exec(line)
      // 对局信息
      if (data && data.length) {
        const value = data[1]
        // 比赛信息
        if (value.indexOf('Event') !== -1) {
          gameInfo.event = replaceAndTrim(value, 'Event')
        }
        // Rated bullet game
        gameInfo.type = gameInfo.event.split(' ')?.[1]
        if (!gameInfo.type) {
          gameInfo.type = gameInfo.event.split(' ')?.[0] || '未知'
        }
        // 直接跳过非标准棋局
        if (value.indexOf('Variant') !== -1)  {
          const Variant = replaceAndTrim(value, 'Variant')
          if (Variant !== 'Standard') {
            console.log("Variant", Variant)
            jumpCount++
            jumpFlag = true
            continue
          }
        }
        // 棋局的唯一hash
        if (value.indexOf('Site') !== -1) {
          gameInfo.hash = replaceAndTrim(value, 'Site').replace("https://lichess.org/", '')
        }
        // 黑方的用户名
        if ((value.indexOf('Black') !== -1) && (value.indexOf('BlackRatingDiff') === -1) && (value.indexOf('BlackElo') === -1)) {
          gameInfo.black = replaceAndTrim(value, 'Black')
        }
        // 白方的用户名
        if ((value.indexOf('White') !== -1) && (value.indexOf('WhiteRatingDiff') === -1) && (value.indexOf('WhiteElo') === -1)) {
          gameInfo.white = replaceAndTrim(value, 'White')
        }
        // 黑白的积分
        if (value.indexOf('BlackElo') !==-1)  {
          gameInfo.blackElo = replaceAndTrim(value, 'BlackElo')
        }
        if (value.indexOf('WhiteElo') !== -1)  {
          gameInfo.whiteElo = replaceAndTrim(value, 'WhiteElo')
        }
        // 这盘棋的ECO和开局名称
        if (value.indexOf('ECO') !== -1)  {
          gameInfo.opening = replaceAndTrim(value, 'ECO')
        }
        if (value.indexOf('Opening') !== -1)  {
          gameInfo.opening += ' ' + replaceAndTrim(value, 'Opening')
        }
        // 时间
        if (value.indexOf('UTCDate') !== -1)  {
          gameInfo.date += replaceAndTrim(value, 'UTCDate')
        }
        if (value.indexOf('UTCTime') !== -1)  {
          gameInfo.date += (' ' + replaceAndTrim(value, 'UTCTime'))
          gameInfo.date = moment(gameInfo.date, 'YYYY.MM.DD HH:mm:ss').add(8,'hours').valueOf() as any
        }
        if (value.indexOf('TimeControl') !== -1)  {
          if (replaceAndTrim(value, 'TimeControl') === '-') {
            gameInfo.time = '无限'
          } else {
            gameInfo.time += (' ' + replaceAndTrim(value, 'TimeControl'))
          }
          // 需要处理一下，前面的数字是秒数
          if (gameInfo.time.indexOf('+') !== -1) {
            const timeInfo = gameInfo.time.split('+')
            const minutes = Number(timeInfo[0]) / 60
            gameInfo.time = minutes + '+' + timeInfo[1]
          }
        }
        // 对局结果
        if (value.indexOf('Result') !== -1)  {
          gameInfo.result += replaceAndTrim(value, 'Result')
        }
      }
    }
    return {gameCount, jumpCount}
  }

  parseFgnMove(str: string): {[key: string]: any}[] {
    const list = str.split('.')
    const res = []
    for (const item of list) {
      // 跳过开头的对局名文字
      if (item[0] === 'A' || item[0] === 'B' || item[0] === 'C' || item[0] === 'D' || item[0] === 'E') {
        // 注意不要过滤类似于Be3这样的棋步
        if (!_.isNaN(Number(item[1]))) continue
      }
      const moveList = item.split(' ')
      for (const move of moveList) {
        if (!_.isNaN(parseFloat(move))) continue
        if (!move || move === '') continue
        res.push({move})
      }
    }
    return res
  }

  // 分析一整盘棋 传入fen则是从某一个局面开始分析
  async analyzeGame(moveList: {move: string}[], fen=utils.newGame, fastAnalyze=false): Promise<any> {
    console.log("fastAnalyze", fastAnalyze)
    let index = 0
    const result = []
    for (const item of moveList) {
      console.log("当前分析步骤: ", item.move)
      const res: {
        index: number,
        move: string,
        currentValue: number,
        type: '不精确' | '错着' | '败着' | '最佳' | '普通',
        valueDiff: any,
        bestmove: string,
      } = {
        index, move: item.move,
        currentValue: 0,
        valueDiff: 0,
        type: '普通',
        bestmove: null
      }
      index++
      const nodes = fastAnalyze ? 20000 : null
      const analyzeRes = await this.analyze(fen, nodes)
      const parsed = this.parseAnalyze(fen, analyzeRes)
      const bestmove = parsed.bestmove
      res.bestmove = bestmove

      const parsedChoose = parsed.moveList
      fen = this.move(fen, item.move)
      let isBest = false
      if (item.move === bestmove) {
        isBest = true
        res.type = '最佳'
      }
      // 找到前五选里面有没有用户的步骤
      // 如果是最优选就不算分差
      // 如果不是，分差是和最优选择作差
      let found = false
      let bestmoveValue = 0
      for (const choose of parsedChoose) {
        if (choose.move === bestmove) {
          if (isBest) {
            res.currentValue = choose.value
            res.valueDiff = 0
            emitvue.$emit("parseResult", res)
            result.push(res)
            break
          }
          bestmoveValue = choose.value
        }
        // 前五选
        if (choose.move === item.move) {
          found = true
          res.currentValue = choose.value
        }
      }
      if (isBest) continue
      if (!found) {
        const notFoundMoveAnalyze = await this.analyzeCurrentFenValue(fen)
        if (String(notFoundMoveAnalyze.value).indexOf('M') === -1) {
          res.currentValue = Number(notFoundMoveAnalyze.value)
        } else {
          res.currentValue = notFoundMoveAnalyze.value as any
        }
      }
      if (String(bestmoveValue).indexOf("M") !== -1) {
        if (String(res.currentValue).indexOf("M") === -1) {
          res.valueDiff = '无穷大'
          res.type = '败着'
        }
      } else {
        if (String(res.currentValue).indexOf('M') !== -1) {
          res.valueDiff = 0
        } else {
          res.valueDiff = Math.abs(bestmoveValue - res.currentValue)
        }
        if (res.valueDiff > 0.5) res.type = '不精确'
        if (res.valueDiff > 1) res.type = '错着'
        if (res.valueDiff > 2) res.type = '败着'
        res.valueDiff = Number(res.valueDiff.toFixed(2))
      }
      emitvue.$emit("parseResult", res)
      result.push(res)
    }
    // emitvue.$emit("parseEnd", null)
    return result
  }


  async analyzeAll(fastAnalyze=false): Promise<any> {
    const data = await Database.getNotAnalyzedGame()
    console.log("共需要分析", data.length, '局棋')
    Chessmate.analyzeCount = 0
    Chessmate.analyzeFlag = false
    for (const item of data) {
      Chessmate.analyzeCount++
      console.log("正在分析: ", item, "对局数:", Chessmate.analyzeCount)
      const moveList = await this.analyzeGame(JSON.parse( hex2str(item.data)), utils.newGame, fastAnalyze)
      await Database.saveAnalyzeResult(item, moveList)
      console.log("获取错题")
      await this.getExercise(item, moveList)
      console.log("分析完成")
      if (Chessmate.analyzeFlag) {
        console.log("退出分析")
        emitvue.$emit('analyzeStop', null)
        break
      }
    }
    emitvue.$emit('analyzeRealStop', null)
    return
  }

  // 根据分析的结果生成错题集
  async getExercise(gameData: {hash: string, white: string, black: string, gameDate: string, type: string}, moveList: {
    bestmove: string,
    currentValue: string,
    move: string,
    type: '普通' | '错着' | '败着' | '最佳' | '不精确',
    valueDiff: number
  }[]): Promise<any> {
    const {hash, white, black, gameDate, type} = gameData
    const gameInfo =  {hash, white, black, gameDate, gameType: type}
    let fen = utils.newGame
    let moveNum = 0
    await Database.clearExercise(hash)
    for (const item of moveList) {
      moveNum++
      if (item.type === '普通' || item.type === '最佳') {
        fen = this.move(fen, item.move)
      }
      else {
        const data: {
          hash: string,
          fenHash?: string,
          badMove?: string,
          correctMove?: string,
          value?: any,
          learn: 0,
          type: '错着' | '败着' | '不精确',
          disabled: 0,
          moveNum: number,
          analyzed: 0,
        } = {
          hash, learn: 0, disabled: 0, fenHash: str2hex(fen),
          badMove: item.move,
          correctMove: item.bestmove,
          type: item.type,
          value: item.valueDiff,
          moveNum,
          analyzed: 0,
        }
        await Database.saveExercise(data, gameInfo)
        fen = this.move(fen, item.move)
        // 防止高并发写入导致漏数据
        await sleep(200)
      }
    }
  }

  // 重新分析所有习题
  async analyzeExercise(): Promise<any> {
    const exerciseInfo = await Database.getExerciseForLearn({analyze: 0, order: 'desc'})
    console.log("exerciseList", exerciseInfo )
    const data = exerciseInfo.data
    console.log("共需要分析", data.length, '个错题')
    Chessmate.analyzeCount = 0
    Chessmate.analyzeFlag = false
    for (const item of data) {
      Chessmate.analyzeCount++
      console.log("正在分析: ", item, "对局数:", Chessmate.analyzeCount)
      const fen = hex2str(item.fenHash)
      // 获取当前局面的最佳后续
      const resultBest = await this.analyzeCurrentFenValue(fen, null, 1500000)
      const tempFen = this.move(fen, item.badMove)
      const resultBad = await this.analyzeCurrentFenValue(tempFen, null, 1500000)
      const bestmove = resultBest.move
      let value: any = Number(String(Math.abs(resultBad.value - resultBest.value).toFixed(2)))
      if (bestmove === item.badMove) {
        await Database.deleteExercise(item.hash, item.fenHash, item.moveNum)
      }

      let type = '普通'
      if (item.badMove === bestmove) {
        type = '最佳'
      }
      if (_.isNaN(value)) {
        value = '无穷大'
        type = '败着'
        console.log("检测到错过了杀棋")
      } else {
        if (value > 0.5) type = '不精确'
        if (value > 1) type = '错着'
        if (value > 2) type = '败着'
      }

      if (type === '普通' || type ==='最佳') {
        await Database.deleteExercise(item.hash, item.fenHash, item.moveNum)
        console.log("删除不是错题的习题")
        Chessmate.analyzeDelete++
        continue
      }
      await Database.updateExerciseAnalyzeResult(item.hash, item.fenHash, item.moveNum, bestmove, value)
      if (Chessmate.analyzeFlag) {
        console.log("退出分析")
        emitvue.$emit('analyzeStop', null)
        break
      }
    }
  }
}